fix(payments): clawback refunded transactions from payouts owed (#498) - #506
Merged
Merged
Conversation
A transaction flipping to refunded after its payout was already recorded previously just vanished from the successful-only query with no trace, silently absorbing the loss. Refunded transactions are now included and scaled by the same split, surfaced as a distinct clawback figure that reduces netOwed on the next cycle instead of disappearing. Closes #498. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Owner
Author
|
Verification screenshot for the #498 refund clawback fix. Seeded a $100 USD PayPal sale with an 80% split snapshot, refunded after an $80 manual payout had already been recorded for it. The payouts page now shows: Collected $0.00 (the refunded sale no longer counts as collected), Paid so far $80.00, a −$80.00 clawback figure in red, Owed $0.00, and a red banner summarizing the total clawback — visible instead of silently netted away. |
3 tasks
guillermoscript
marked this pull request as ready for review
July 24, 2026 16:48
Closed
25 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Description
Part of epic #493 (Phase 2 — payout money-accuracy). Branches directly off
master, since #503/#504/#505 are all merged.markPayoutPaidwas a one-way ratchet: once a payout was recorded, nothing re-checked whether the underlyingtransactionsit was based on later flipped torefunded. SincegetPayoutsOwed()only ever queriedstatus = 'successful', a refunded transaction simply vanished from the calculation with no trace —netOwedwould silently drop to$0.00for that tenant going forward, absorbing the loss with no record that a refund had happened.Changes made
lib/payments/payouts-owed.ts—PlatformSettledTxngained astatus: 'successful' | 'refunded'field.computeOwedBalancesnow includes refunded transactions in the aggregation instead of relying on the caller to filter them out: arefundedtransaction no longer contributes togrossCollected/grossOwed/byProvider, but its scaled amount (same split-percentage logic as any sale — snapshot-or-current) is added to a newclawbackfigure.netOwedis nowmax(grossOwed - alreadyPaid - clawback, 0).CurrencyBalancegained aclawback: numberfield — always present and visible, even when0.app/actions/platform/payouts.ts—getPayoutsOwed()now selectsstatusand queries.in('status', ['successful', 'refunded'])instead of.eq('status', 'successful'), and passesstatusthrough tocomputeOwedBalances.app/[locale]/platform/payouts/page.tsx— adds a "Clawback" column to the by-school table (shown as−$X.XXin red when non-zero,—otherwise) and a red banner above the table summarizing total clawback per currency whenever any tenant has one, so a refund-after-payout is visible rather than silently netted away.Type of change
Closes #498 (Phase 2 sub-task of epic #493).
Testing
npx vitest run tests/unit/payouts-owed.test.ts— 18/18 passing, including 4 new tests specific to No refund/chargeback clawback for manual payouts already marked paid #498 (refund-after-payout produces a visible clawback and reduces netOwed; clawback is reported even with nothing else outstanding; a refund in one currency doesn't clawback a balance in another currency; clawback uses the transaction's own snapshotted split, not the tenant's current one).npm run test:unit— 277/277 passing, no regressions.npm run typecheck— clean.npm run linton changed files — no new errors (one pre-existing unused-var warning inpayouts.tspredates this change, unrelated to it).npm run build— succeeds.status = 'refunded'(80% split snapshot) for a tenant that already had an $80 manual payout recorded against it, then loaded/platform/payoutsas a super admin. Screenshot attached below shows: Collected$0.00(the refunded sale no longer counts as collected), Paid so far$80.00, Clawback−$80.00in red, Owed$0.00, and the red clawback banner summarizing it above the table. Test data cleaned up afterward.QA verification steps
/platform/payouts.statustorefundeddirectly in the DB (simulating a later refund)./platform/payouts— confirm a red "Refund clawback" banner appears, the tenant's row shows a−$X.XXclawback figure, and "Owed" reflects the reduction (floored at$0.00, never negative).Screenshot
Attached as a PR comment: the payouts page showing the clawback banner and table row for the refund-after-payout scenario.